Popular Searches
Popular Course Categories
Popular Courses

Storing and Retrieving Data Using Cloud Firestore

Storing and Retrieving Data Using Cloud Firestore

Firebase Integration and Cloud Services

Storing and Retrieving Data Using Cloud Firestore

Course: iOS App Development

Topic: Storing and Retrieving Data Using Cloud Firestore

Course Link: iOS Training at JustAcademy

Demo Registration: Register for a Free Course Demo

JustAcademy Website: Visit JustAcademy


1. Introduction to Cloud Firestore

Cloud Firestore is a flexible, scalable, cloud-hosted NoSQL database provided by Firebase. It allows iOS applications to store, retrieve, update, and delete data without requiring developers to build and maintain their own database server.

In an iOS application, Firestore can be used to manage user profiles, notes, product information, chat messages, task lists, booking records, and other application data. The Firebase SDK provides Swift APIs for communicating with Firestore.

Firestore supports real-time listeners, queries, offline caching, and integration with Firebase Authentication and Firebase Security Rules. These capabilities make it useful for applications that need synchronized data across multiple devices.

2. Learning Objectives

  • Understand the structure and purpose of Cloud Firestore.
  • Learn the difference between collections and documents.
  • Configure Firebase and Firestore in an iOS project.
  • Store data from Swift and SwiftUI applications.
  • Retrieve individual documents and collections.
  • Perform queries using filters, sorting, and limits.
  • Update and delete stored documents.
  • Listen for real-time changes in database data.
  • Handle errors and understand offline behavior.
  • Protect application data using Firebase Security Rules.

3. What Is Cloud Firestore?

Cloud Firestore stores information in documents organized into collections. A document contains fields and values, such as strings, numbers, Boolean values, timestamps, arrays, maps, and references to other documents.

Unlike a traditional relational database, Firestore does not require tables with a fixed schema or SQL joins. Applications organize information into collections and documents and retrieve data using Firestore queries.

Key Features

Feature Description
NoSQL data model Stores data in collections and documents instead of relational tables.
Real-time listeners Notifies the application when observed documents change.
Offline support Can cache data locally and synchronize supported changes when connectivity returns.
Flexible queries Supports filtering, ordering, and limiting query results.
Scalability Managed cloud infrastructure supports growing application workloads.
Security Rules Controls which database operations users are authorized to perform.
Firebase integration Works with services such as Firebase Authentication and Cloud Functions.

4. Understanding Collections and Documents

4.1 Collections

A collection is a group of documents. For example, a notes application might have a collection named notes that contains individual note documents.

notes
  ├── document_001
  ├── document_002
  └── document_003

4.2 Documents

A document is a record containing fields and values. A note document might contain a title, description, creation timestamp, and owner identifier.

{
  "title": "Learn Swift",
  "description": "Practice asynchronous programming",
  "isCompleted": false,
  "ownerId": "USER_ID",
  "createdAt": "Firestore Timestamp"
}

The example illustrates the document structure. In an actual Firestore database, the timestamp field should be stored as a Firestore timestamp value rather than as the literal text shown above.

4.3 Example Database Structure

users
  └── USER_ID
       ├── name: "Aarav"
       ├── email: "[email protected]"
       └── notes
            ├── NOTE_ID_1
            │    ├── title: "Swift Basics"
            │    └── isCompleted: false
            └── NOTE_ID_2
                 ├── title: "Firestore"
                 └── isCompleted: true

Here, each user has a document in the users collection, and the user's notes are stored in a subcollection named notes.

5. Prerequisites for Using Firestore in iOS

Before integrating Cloud Firestore, make sure you have:

  • A Mac computer with a compatible version of Xcode.
  • An iOS project created using Swift or SwiftUI.
  • A Firebase project in the Firebase Console.
  • The Firebase Apple-platform SDK added to the Xcode project.
  • A registered iOS application with its bundle identifier.
  • A Firestore database created in the Firebase Console.
  • Firebase Security Rules appropriate for the application's data.

6. Setting Up Firebase and Cloud Firestore

Step 1: Create a Firebase Project

  1. Open the Firebase Console.
  2. Sign in with your Google account.
  3. Select the option to create a project.
  4. Enter a project name and follow the setup instructions.
  5. Enable Google Analytics if it is useful for your application.

Step 2: Register Your iOS App

  1. Open the Firebase project.
  2. Add an Apple or iOS application.
  3. Enter the bundle identifier from your Xcode project.
  4. Download the GoogleService-Info.plist configuration file.
  5. Add the file to the appropriate target in Xcode and verify that it is included in the application target.

The bundle identifier entered in Firebase must match the bundle identifier used by the application.

Step 3: Add the Firebase SDK

For a new project, Swift Package Manager is the recommended installation method for the Firebase Apple-platform SDK. In Xcode, select File > Add Package Dependencies and enter the official Firebase Apple SDK repository:

https://github.com/firebase/firebase-ios-sdk

Select the Firebase products required by your application, including FirebaseFirestore. If you use Firebase Authentication, add the appropriate Authentication product as well.

Step 4: Initialize Firebase

For a SwiftUI application, Firebase is commonly initialized through an application delegate attached to the app lifecycle.

import SwiftUI
import FirebaseCore

class AppDelegate: NSObject, UIApplicationDelegate {
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions:
            [UIApplication.LaunchOptionsKey: Any]? = nil
    ) -> Bool {
        FirebaseApp.configure()
        return true
    }
}

@main
struct FirestoreDemoApp: App {
    @UIApplicationDelegateAdaptor(AppDelegate.self)
    var delegate

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

For a UIKit application, initialize Firebase in the application delegate's launch method before using Firebase services. Follow the current Firebase setup instructions if your application uses a different lifecycle or configuration.

Step 5: Create a Cloud Firestore Database

  1. Open the Firestore Database section in the Firebase Console.
  2. Select the option to create a database.
  3. Choose a database location appropriate for your application and users.
  4. Configure the database's initial security settings.
  5. Publish suitable Security Rules before releasing the application.

Do not leave production data accessible through unrestricted test rules.

7. Importing and Accessing Firestore in Swift

After adding the SDK and configuring Firebase, import the Firestore module and obtain a database instance.

import FirebaseFirestore

let db = Firestore.firestore()

Access the database only after Firebase has been configured. In a larger application, a repository or service object can manage Firestore access instead of repeatedly creating database references throughout the UI.

8. Storing Data in Cloud Firestore

Firestore provides methods for creating documents and writing field values. Common operations include addDocument(data:), setData(_:), and updateData(_:).

8.1 Add a Document with an Automatically Generated ID

Use addDocument(data:) when you want Firestore to generate a unique document ID.

import FirebaseFirestore

let db = Firestore.firestore()

func addNote() {
    let note: [String: Any] = [
        "title": "Learn Swift",
        "description": "Practice Firestore operations",
        "isCompleted": false,
        "createdAt": FieldValue.serverTimestamp()
    ]

    db.collection("notes").addDocument(data: note) {
        error in

        if let error = error {
            print("Failed to save note: \(error.localizedDescription)")
        } else {
            print("Note saved successfully")
        }
    }
}

Explanation:

  • collection("notes") identifies the collection.
  • addDocument(data:) creates a document with an automatically generated ID.
  • FieldValue.serverTimestamp() requests a server-generated timestamp for the write.
  • The completion handler reports whether the write succeeded or failed.

8.2 Store Data with a Specific Document ID

Use document("documentID").setData(...) when your application already knows the document ID, such as a user ID.

func saveUserProfile(userID: String) {
    let profile: [String: Any] = [
        "name": "Aarav",
        "email": "[email protected]",
        "updatedAt": FieldValue.serverTimestamp()
    ]

    db.collection("users")
        .document(userID)
        .setData(profile) { error in
            if let error = error {
                print("Profile save failed: \(error.localizedDescription)")
            } else {
                print("Profile saved successfully")
            }
        }
}

If a document with that ID already exists, setData without merge options replaces its existing field data with the supplied data. Use merge options when you want to preserve other existing fields.

8.3 Set Data Without Replacing Existing Fields

func updateProfileName(userID: String) {
    db.collection("users")
        .document(userID)
        .setData(
            ["name": "Aarav Sharma"],
            merge: true
        ) { error in
            if let error = error {
                print("Update failed: \(error.localizedDescription)")
            } else {
                print("Profile name updated")
            }
        }
}

The merge: true option merges the provided fields into the existing document instead of replacing all its fields.

8.4 Store Strongly Typed Swift Data

For larger applications, representing data with a Swift structure makes the code easier to understand and maintain.

import Foundation
import FirebaseFirestore

struct Note: Identifiable, Codable {
    @DocumentID var id: String?
    var title: String
    var description: String
    var isCompleted: Bool
    var createdAt: Date?
}

Firestore supports Codable-based serialization through its Apple SDK. The document ID property can be populated from the Firestore document ID during decoding. If your model uses a server timestamp, remember that the timestamp may not have resolved to a final value immediately after a local write.

9. Retrieving a Single Document

Use getDocument to fetch a particular document. This is suitable when you need a single read rather than a continuously updated result.

func fetchNote(noteID: String) {
    db.collection("notes")
        .document(noteID)
        .getDocument { snapshot, error in
            if let error = error {
                print("Read failed: \(error.localizedDescription)")
                return
            }

            guard let snapshot = snapshot,
                  snapshot.exists,
                  let data = snapshot.data() else {
                print("Note does not exist")
                return
            }

            let title = data["title"] as? String ?? "Untitled"
            let description =
                data["description"] as? String ?? ""

            print("Title: \(title)")
            print("Description: \(description)")
        }
}

Important checks: handle request errors, verify that the document exists, and safely convert field values into the expected Swift types.

10. Retrieving All Documents from a Collection

Use getDocuments to retrieve the documents that match a collection query at the time of the request.

func fetchAllNotes() {
    db.collection("notes")
        .getDocuments { snapshot, error in
            if let error = error {
                print("Unable to retrieve notes: \(error.localizedDescription)")
                return
            }

            guard let documents = snapshot?.documents else {
                print("No documents returned")
                return
            }

            for document in documents {
                let data = document.data()
                let title = data["title"] as? String ?? "Untitled"
                print("\(document.documentID): \(title)")
            }
        }
}

This approach is useful for one-time reads. If the interface must remain synchronized with database changes, use a snapshot listener instead.

11. Retrieving Data with Codable

Using a model type instead of manually reading every field reduces repeated conversion code. The following example decodes one Firestore document into the Note model defined earlier.

func fetchTypedNote(noteID: String) {
    db.collection("notes")
        .document(noteID)
        .getDocument { snapshot, error in
            if let error = error {
                print("Firestore error: \(error.localizedDescription)")
                return
            }

            guard let snapshot = snapshot,
                  snapshot.exists else {
                print("Note not found")
                return
            }

            do {
                let note = try snapshot.data(as: Note.self)
                print("Note title: \(note.title)")
            } catch {
                print("Decoding failed: \(error.localizedDescription)")
            }
        }
}

Codable decoding may fail if stored field names, types, or required values do not match the Swift model. Keep the model and Firestore document schema consistent.

12. Querying Data with Filters

Firestore queries allow an application to retrieve only documents matching specified conditions.

12.1 Filter by a Boolean Field

func fetchCompletedNotes() {
    db.collection("notes")
        .whereField("isCompleted", isEqualTo: true)
        .getDocuments { snapshot, error in
            if let error = error {
                print("Query failed: \(error.localizedDescription)")
                return
            }

            let documents = snapshot?.documents ?? []
            for document in documents {
                print(document.data())
            }
        }
}

12.2 Filter by a String Field

func fetchNotesByTitle(_ title: String) {
    db.collection("notes")
        .whereField("title", isEqualTo: title)
        .getDocuments { snapshot, error in
            if let error = error {
                print(error.localizedDescription)
                return
            }

            for document in snapshot?.documents ?? [] {
                print(document.documentID)
            }
        }
}

Equality queries match stored field values. Firestore does not automatically provide unrestricted full-text search; for advanced text search, consider a dedicated search service.

12.3 Order Results and Limit the Number of Documents

func fetchLatestNotes() {
    db.collection("notes")
        .order(by: "createdAt", descending: true)
        .limit(to: 10)
        .getDocuments { snapshot, error in
            if let error = error {
                print("Unable to load recent notes: \(error.localizedDescription)")
                return
            }

            for document in snapshot?.documents ?? [] {
                print(document.data())
            }
        }
}

Ordering by a field excludes documents that do not contain that field. Queries that combine certain filters and ordering may require a composite index. Firestore normally provides a console link when a required index is missing.

13. Updating Existing Data

Use updateData to modify specific fields in an existing document without replacing all its other fields.

func markNoteCompleted(noteID: String) {
    db.collection("notes")
        .document(noteID)
        .updateData([
            "isCompleted": true,
            "updatedAt": FieldValue.serverTimestamp()
        ]) { error in
            if let error = error {
                print("Update failed: \(error.localizedDescription)")
            } else {
                print("Note marked as completed")
            }
        }
}

Unlike setData, updateData fails if the target document does not exist. Choose the method based on whether you intend to create or update the record.

14. Deleting Documents

Use delete() to remove a document.

func deleteNote(noteID: String) {
    db.collection("notes")
        .document(noteID)
        .delete { error in
            if let error = error {
                print("Delete failed: \(error.localizedDescription)")
            } else {
                print("Note deleted successfully")
            }
        }
}

Deleting a Firestore document does not automatically delete documents in its subcollections. If a document has nested collections, design a separate cleanup process when required.

15. Listening for Real-Time Updates

A snapshot listener keeps the application informed about changes to a document or query. It is useful for chat applications, live dashboards, shared task lists, and collaborative applications.

import FirebaseFirestore

var notesListener: ListenerRegistration?

func listenToNotes() {
    notesListener = db.collection("notes")
        .addSnapshotListener { snapshot, error in
            if let error = error {
                print("Listener error: \(error.localizedDescription)")
                return
            }

            guard let documents = snapshot?.documents else {
                return
            }

            for document in documents {
                let title = document.data()["title"] as? String ?? "Untitled"
                print("Updated note: \(title)")
            }
        }
}

func stopListeningToNotes() {
    notesListener?.remove()
    notesListener = nil
}

The listener can emit an initial result and subsequent updates. Retain its registration and remove it when updates are no longer needed, such as when a screen or service is no longer active.

16. Understanding Offline Data and Synchronization

Cloud Firestore supports offline persistence on Apple platforms. The SDK can serve eligible cached data and queue supported local writes for synchronization when network connectivity returns.

  • Online: the SDK communicates with the backend and receives current data subject to connectivity and permissions.
  • Offline: reads may use cached documents; data that has never been cached may be unavailable.
  • Pending writes: local changes can appear in the UI before the server confirms them.
  • Reconnect: queued writes synchronize when connectivity returns, subject to security rules and write conflicts.

Offline persistence is not a substitute for a backup strategy. Avoid assuming that every read is guaranteed to return fresh server data while the device is disconnected.

17. Integrating Firestore with SwiftUI

SwiftUI interfaces often use an observable view model to separate UI state from database operations. This example demonstrates loading a list of note titles using a snapshot listener.

import SwiftUI
import FirebaseFirestore

struct SimpleNote: Identifiable {
    let id: String
    let title: String
}

@MainActor
final class NotesViewModel: ObservableObject {
    @Published var notes: [SimpleNote] = []
    @Published var errorMessage: String?

    private var listener: ListenerRegistration?

    func startListening() {
        listener?.remove()

        listener = Firestore.firestore()
            .collection("notes")
            .addSnapshotListener { [weak self] snapshot, error in
                Task { @MainActor in
                    guard let self = self else { return }

                    if let error = error {
                        self.errorMessage = error.localizedDescription
                        return
                    }

                    self.notes = (snapshot?.documents ?? []).map {
                        document in

                        SimpleNote(
                            id: document.documentID,
                            title: document.data()["title"] as? String
                                ?? "Untitled"
                        )
                    }

                    self.errorMessage = nil
                }
            }
    }

    func stopListening() {
        listener?.remove()
        listener = nil
    }

    deinit {
        listener?.remove()
    }
}

struct NotesListView: View {
    @StateObject private var viewModel = NotesViewModel()

    var body: some View {
        NavigationStack {
            List(viewModel.notes) { note in
                Text(note.title)
            }
            .navigationTitle("My Notes")
            .overlay {
                if let message = viewModel.errorMessage {
                    Text(message)
                        .foregroundStyle(.red)
                        .padding()
                }
            }
            .task {
                viewModel.startListening()
            }
            .onDisappear {
                viewModel.stopListening()
            }
        }
    }
}

This is an introductory pattern. In a production application, avoid registering duplicate listeners during repeated view lifecycle events, and make the view model's loading, empty, and error states explicit.

18. Using Firebase Authentication with Firestore

When data belongs to individual users, Firebase Authentication can identify the signed-in user. The application can store the authenticated user's UID in a document path and use Security Rules to enforce ownership.

import FirebaseAuth
import FirebaseFirestore

func savePrivateNote(title: String) {
    guard let userID = Auth.auth().currentUser?.uid else {
        print("Please sign in first")
        return
    }

    let data: [String: Any] = [
        "title": title,
        "ownerId": userID,
        "createdAt": FieldValue.serverTimestamp()
    ]

    Firestore.firestore()
        .collection("users")
        .document(userID)
        .collection("notes")
        .addDocument(data: data) { error in
            if let error = error {
                print("Could not save private note: \(error.localizedDescription)")
            } else {
                print("Private note saved")
            }
        }
}

Checking the current user in Swift improves the user experience, but it is not sufficient security by itself. Firestore Security Rules must independently enforce access permissions.

19. Firestore Security Rules

Security Rules determine which documents a client can read or modify. The following example allows a signed-in user to read and modify only documents under their own user document and its notes subcollection.

rules_version = '2';

service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read, write: if request.auth != null
                         && request.auth.uid == userId;

      match /notes/{noteId} {
        allow read, write: if request.auth != null
                           && request.auth.uid == userId;
      }
    }
  }
}

This example is an ownership-based starting point, not a complete validation policy. Add field validation, allowed-field checks, and appropriate create, update, and delete restrictions for your application's requirements.

Important: Security Rules are not filters. A query must be compatible with the rules for every document it could return. Test rules using the Firebase Emulator Suite before deploying them.

20. Error Handling and Troubleshooting

Problem Possible Cause Solution
Permission denied Security Rules reject the operation or the user lacks authorization. Verify authentication, document ownership, query constraints, and rules.
Document not found The requested document ID does not exist. Check the collection name, ID, and existence of the document.
Missing index A query requires a composite index. Use the error's index creation link and review the query design.
Decoding error Stored values do not match the Swift model. Verify field names, types, optional fields, and timestamp handling.
Data appears stale The device may be offline or displaying cached data. Check connectivity and account for Firestore's offline behavior.
Writes appear pending The device has not yet synchronized the write with the server. Check connectivity and handle server-side failures.
Listener keeps running The listener registration was not removed. Retain the ListenerRegistration and call remove() when appropriate.
Firebase app configuration error The configuration file or initialization is incorrect. Verify the bundle identifier, target membership, and initialization order.

21. Best Practices for Cloud Firestore

  • Use descriptive collection names and consistent field names.
  • Represent application records with well-defined Swift model types.
  • Use document IDs deliberately and avoid depending on generated IDs for business meaning.
  • Retrieve only the documents and fields your application needs.
  • Use filters, limits, pagination, and suitable indexes for large collections.
  • Use snapshot listeners only when ongoing updates are needed.
  • Remove listeners when they are no longer required.
  • Use server timestamps for reliable server-side event timing.
  • Handle permission errors, network failures, and decoding problems.
  • Use Firebase Authentication and restrictive Security Rules for private data.
  • Never place server credentials or service-account private keys inside an iOS app.
  • Monitor reads, writes, deletes, storage, and query patterns to manage cost.
  • Test database rules and application behavior before production release.

22. Practical Project: Build a Firestore Notes App

Create a small iOS application where users can add notes, view saved notes, mark notes as completed, and delete notes.

Project Requirements

  1. Create a Firebase project and register the iOS application.
  2. Add the Firebase SDK and initialize the app.
  3. Create a Firestore collection or a user-specific notes subcollection.
  4. Build a SwiftUI form with title and description fields.
  5. Save new notes using addDocument(data:).
  6. Retrieve notes using a collection query or snapshot listener.
  7. Display notes in a SwiftUI List.
  8. Update completion status using updateData.
  9. Delete a selected note using delete().
  10. Show useful loading, empty, and error states.
  11. Protect private notes using Firebase Authentication and Security Rules.
  12. Test the app with multiple records and simulated network interruptions.

Sample Data for Testing

title description isCompleted
Learn Swift Practice Swift language fundamentals false
Study Firestore Implement database operations true
Build an iOS app Create and test a sample project false

23. Practice Exercises

  1. Create a products collection and save three product documents.
  2. Retrieve a product using its document ID.
  3. Query all notes whose isCompleted field is false.
  4. Order notes by creation time and retrieve only the five most recent records.
  5. Update a document's title without overwriting its other fields.
  6. Delete one selected document and verify the result.
  7. Use a snapshot listener to update a list when a document changes.
  8. Write Security Rules that restrict private notes to their owners.
  9. Test what happens when a user is signed out or offline.

24. Interview Questions and Answers

Q1. What is Cloud Firestore?

Cloud Firestore is a cloud-hosted NoSQL database that stores application data in collections and documents and supports queries, real-time listeners, and offline persistence.

Q2. What is the difference between a collection and a document?

A collection groups documents, while a document stores fields and values representing an individual record.

Q3. What is the difference between addDocument and setData?

addDocument(data:) creates a document using an automatically generated ID. setData(_:) writes to a specified document reference and can replace existing fields unless merge behavior is requested.

Q4. What is the difference between getDocument and addSnapshotListener?

getDocument performs a one-time document read. addSnapshotListener observes changes and delivers updated snapshots while the listener remains active.

Q5. What does updateData do?

updateData changes specified fields in an existing document and fails if that document does not exist.

Q6. How can you retrieve documents matching a condition?

Use Firestore query methods such as whereField, optionally combined with ordering, limits, and other supported query constraints.

Q7. How does Firestore support offline access?

Firestore can use cached documents and synchronize supported local writes when connectivity returns. The availability and freshness of data depend on the local cache and network state.

Q8. How do you secure user-specific data?

Use Firebase Authentication to identify the user and Firestore Security Rules to enforce that the user can access only authorized documents.

Q9. What is a Firestore index?

An index helps Firestore efficiently locate documents that satisfy a query. Some combinations of filters and sorting require a composite index.

Q10. Why should listeners be removed?

Removing unneeded listeners stops further observation and helps prevent unnecessary resource usage and duplicate updates.

Q11. Does deleting a document delete its subcollections?

No. Subcollection documents are not automatically removed when their parent document is deleted. They require a separate cleanup strategy.

Q12. Why might a Firestore query fail even when the user has access to some documents?

Security Rules evaluate whether a query's potential result set is permitted. The query constraints must be compatible with the rules; rules do not filter unauthorized results automatically.

25. Quick Revision Checklist

  • Cloud Firestore stores data in collections and documents.
  • Use addDocument for automatically generated document IDs.
  • Use setData to write to a known document reference.
  • Use getDocument or getDocuments for one-time reads.
  • Use whereField to filter query results.
  • Use updateData to update selected fields.
  • Use delete() to remove a document.
  • Use addSnapshotListener for real-time updates.
  • Remove listeners when they are no longer needed.
  • Use Codable models for structured Swift data where appropriate.
  • Understand offline caching and pending writes.
  • Secure private data using Authentication and Firestore Security Rules.
  • Test queries, permissions, and error handling before releasing the app.

26. Official Learning Resources

27. Conclusion

Cloud Firestore provides iOS developers with a practical way to store, retrieve, query, update, and delete cloud-hosted application data. By understanding collections, documents, Swift data models, real-time listeners, offline behavior, and Security Rules, developers can build more responsive and reliable applications.

Practice these concepts by building a small notes application, testing database operations, and protecting user data with Firebase Authentication and restrictive Security Rules.

Explore the iOS course: iOS App Development Training

Register for a demo: JustAcademy Course Demo Registration

Visit the website: JustAcademy

Previous Next
whatsapp